/*
 * @(#)ConfigDOMBuilder.java $version 2010. 8. 11
 *
 * Copyright 2007-2010 NHN Corp. All rights Reserved. 
 * NHN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */
package com.nhncorp.usf.core.parser;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.validator.Field;
import org.apache.commons.validator.Form;
import org.apache.commons.validator.Validator;
import org.apache.commons.validator.ValidatorResources;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.nhncorp.lucy.common.util.SchemaEntityResolver;
import com.nhncorp.lucy.common.util.XMLDOMUtil;
import com.nhncorp.usf.core.Constants;
import com.nhncorp.usf.core.UsfDispatcher;
import com.nhncorp.usf.core.config.AttributeInfo;
import com.nhncorp.usf.core.config.AttributeInfo.ATTRIBUTE_NAME;
import com.nhncorp.usf.core.config.runtime.Action;
import com.nhncorp.usf.core.config.runtime.Assign;
import com.nhncorp.usf.core.config.runtime.BusinessObject;
import com.nhncorp.usf.core.config.runtime.BusinessObjectGroup;
import com.nhncorp.usf.core.config.runtime.ConditionInfo;
import com.nhncorp.usf.core.config.runtime.ConditionInfoGroup;
import com.nhncorp.usf.core.config.runtime.Method;
import com.nhncorp.usf.core.config.runtime.PageInfo;
import com.nhncorp.usf.core.config.runtime.ParameterInfo;
import com.nhncorp.usf.core.config.runtime.PermissionInfo;
import com.nhncorp.usf.core.config.runtime.ResultCondition;
import com.nhncorp.usf.core.config.runtime.ResultPageInfo;
import com.nhncorp.usf.core.config.runtime.ResultPageInfos;
import com.nhncorp.usf.core.config.runtime.RuntimeInterceptorConfig;
import com.nhncorp.usf.core.config.runtime.ScriptInfo;
import com.nhncorp.usf.core.config.runtime.Service;
import com.nhncorp.usf.core.config.runtime.ServiceCondition;
import com.nhncorp.usf.core.config.runtime.ServiceConditionGroup;
import com.nhncorp.usf.core.config.runtime.ServiceImpl;
import com.nhncorp.usf.core.config.runtime.ResultPageInfo.DISPATCHER_TYPE;
import com.nhncorp.usf.core.service.ServiceType;
import com.nhncorp.usf.core.util.StringUtil;

/**
 * Dynamic Page Configuration 정보 빌드.
 * 
 * @author Web Platform Development Team.
 */
@SuppressWarnings("deprecation")
public class ConfigDOMBuilder implements ConfigBuilder {
    @SuppressWarnings("unused")
    private static final Exception NULL_POINTER_EXCEPTION = null;

    static Log log = LogFactory.getLog(ConfigDOMBuilder.class);

    static SchemaEntityResolver entityResolver = new SchemaEntityResolver();

    static {
        String schemaVersion = AttributeInfo.getInstance().getAttribute(
                ATTRIBUTE_NAME.schemaVersion.name());

        Properties urlMap = new Properties();
        String url = null;
        String location = "META-INF/schema/usf-core.xsd";

        if ("1.0".equals(schemaVersion)) {
            url = "http://dev.naver.com/xsd/usf/1.0/usf-core.xsd";
        } else {
            url = "http://dev.naver.com/xsd/usf/usf-core.xsd";
        }

        log.debug("Schema URL : " + url);
        urlMap.put(url, location);
        entityResolver.setSchemaProperty(urlMap);
    }

    /** configuration.xml 의 Attribute 에 ignore-exception 설정 */
    private String globalIgnoreException = AttributeInfo.getInstance()
            .getAttribute(ATTRIBUTE_NAME.ignoreException.name());

    /** 현재 파싱 중인 Action 의 ignore-exception 설정 여부 */
    private String actionIgnoreException;

    /**
     * Dynamic Page Configuration 정보를 읽어서 {@link PageInfo} 생성.
     * 
     * @param inputStream
     *            Template file의 input stream
     * @return Configuation 정보를 담은 {@link PageInfo}
     * @throws Exception
     *             the Exception
     */
    public PageInfo build(InputStream inputStream) throws Exception {
        Document document = XMLDOMUtil.loading(inputStream, true,
                entityResolver);
        Node pcfgRunElement = document.getDocumentElement();

        // build current page info
        PageInfo pageInfo = new PageInfo(XMLDOMUtil.getValue(pcfgRunElement,
                "@id"), XMLDOMUtil.getValue(pcfgRunElement, "@default",
                Constants.DEFAULT_ACTION_ID));
        extractGlobalPermission(pcfgRunElement, pageInfo);
        extractGlobalResult(pcfgRunElement, pageInfo);
        extractAction(pcfgRunElement, pageInfo);

        // extractScript(pcfgRunElement, pageInfo);

        document = null;
        return pageInfo;
    }

    /**
     * Dynamic Page Configuration 정보를 읽어서 {@link PageInfo} 생성.
     * 
     * @param inputStream
     *            Template file의 input strem
     * @param templatePath
     *            Template file들이 위치하고 있는 root directory
     * @return Configuation 정보를 담은 {@link PageInfo}
     * @throws Exception
     *             the Exception
     */
    public PageInfo build(InputStream inputStream, String templatePath)
            throws Exception {
        Document document = XMLDOMUtil.loading(inputStream, true,
                entityResolver);

        document = extractIncludeBlock(document, templatePath);
        Node pcfgRunElement = document.getDocumentElement();

        // include 되는 block 일 경우 파싱하지 않는다.
        if ("included-block".equals(pcfgRunElement.getNodeName())) {
            return null;
        }

        // build current page info
        PageInfo pageInfo = new PageInfo(XMLDOMUtil.getValue(pcfgRunElement,
                "@id"), XMLDOMUtil.getValue(pcfgRunElement, "@default",
                Constants.DEFAULT_ACTION_ID));

        extractAction(pcfgRunElement, pageInfo);
        extractInclude(pcfgRunElement, pageInfo);
        extractGlobalPermission(pcfgRunElement, pageInfo);
        extractGlobalResult(pcfgRunElement, pageInfo);

        // build parent page info
        String parentPage = pageInfo.getInclude();
        PageInfo parentInfo = null;

        if (StringUtil.isNotEmpty(parentPage, false)) {
            // Added by imjavaj
            // TODO Add parent node support in dependency jar
            parentPage = templatePath + File.separator + parentPage
                    + Constants.RUNTIME_XML;
            parentPage = FilenameUtils.separatorsToUnix(parentPage);

            // recursivly generate parentInfo
            FileInputStream fin = null;
            try {
                fin = new FileInputStream(new File(parentPage));
                parentInfo = build(fin, templatePath);
            } catch (Exception e) {
                throw e;
            } finally {
                if (fin != null) {
                    fin.close();
                }
            }
            pageInfo.addParent(parentInfo);

        }

        document = null;
        return pageInfo;
    }

    /**
     * Dynamic Page Configuration 정보를 읽어서 {@link PageInfo} 생성.
     * 
     * @param dynamicConfigInputStream
     *            Template file의 input strem
     * @param validatorInputStream
     *            validator가 정의된 파일
     * @param templatePath
     *            Template file들이 위치하고 있는 root directory
     * @return Configuation 정보를 담은 {@link PageInfo}
     * @throws Exception
     *             the Exception
     */
    @SuppressWarnings("unchecked")
    public PageInfo build(InputStream dynamicConfigInputStream,
            InputStream validatorInputStream, String templatePath)
            throws Exception {
        PageInfo pageInfo = build(dynamicConfigInputStream, templatePath);

        // Validator 파일 설정
        if (validatorInputStream != null && pageInfo != null) {
            ValidatorResources internalValidators = null;
            InputStream internalValidatorIs = null;

            try {
                internalValidatorIs = ConfigDOMBuilder.class
                        .getResourceAsStream("/META-INF/internal-validators.xml");
                internalValidators = new ValidatorResources(internalValidatorIs);
            } finally {
                internalValidatorIs.close();
            }

            ValidatorResources resources = new ValidatorResources(
                    validatorInputStream);

            for (java.util.Map.Entry<String, Action> entry : pageInfo
                    .getActionInfos().entrySet()) {
                Form form = resources.getForm(Locale.KOREA, entry.getKey());

                if (form == null) {
                    continue;
                }

                List<Field> fields = form.getFields();

                for (Field field : fields) {
                    List<String> depends = field.getDependencyList();

                    for (String depend : depends) {
                        resources.addValidatorAction(internalValidators
                                .getValidatorAction(depend));
                    }
                }

                Validator validator = new Validator(resources, entry.getKey());
                pageInfo.setValidator(entry.getKey(), validator);
            }

            try {
                validatorInputStream.close();
                internalValidatorIs.close();
            } catch (Exception e) {
                throw e;
            } finally {
                validatorInputStream.close();
            }
        }

        return pageInfo;
    }

    /**
     * {@code pcfgRunElement} 에서 include-block 을 추출해서 해당 부분을 include-block 의 src
     * 로 대체한다. 대체한 document 의 validation test 도 함께 한다.
     * 
     * @param document
     *            Document
     * @param templatePath
     *            template file path
     * @throws Exception
     *             exception
     * @return Document
     * */
    private Document extractIncludeBlock(Document document, String templatePath)
            throws Exception {
        NodeList includeBlocks = document.getElementsByTagName("include-block");
        boolean hasIncludeBlock = includeBlocks.getLength() > 0;

        while (includeBlocks.getLength() > 0) {
            Node includeBlock = includeBlocks.item(0);
            String path = getIncludeFilePath(templatePath, includeBlock);
            importIncludeBlock(includeBlock, path);
        }

        // includeBlock 을 치환했을 경우 document validation 을 다시 한다.
        if (hasIncludeBlock) {
            document = validateDocument(document);
        }

        return document;
    }

    /**
     * include-block 이 포함된 경우 include 하고나서 {@code document} 의 validate 검사를 한다.
     * 
     * @param document
     *            Document
     * @return Document
     * @throws Exception
     *             exception
     * */
    private Document validateDocument(Document document) throws Exception {
        StringBuffer sb = XMLDOMUtil.printXML(document);
        ByteArrayInputStream is = new ByteArrayInputStream(sb.toString()
                .getBytes());
        Document validDom = XMLDOMUtil.loading(is, true, entityResolver);
        return validDom;
    }

    /**
     * include-block src 의 real path 를 반환한다.
     * 
     * @param templatePath
     *            template file path
     * @param includeBlock
     *            included block node
     * @return filePath
     * */
    private String getIncludeFilePath(String templatePath, Node includeBlock) {
        String src = XMLDOMUtil.getValue(includeBlock, "@src");
        String path = templatePath + src;
        path = FilenameUtils.separatorsToUnix(path);
        return path;
    }

    /**
     * templatePath 를 읽어서 dom 파싱해서 반환한다. <br/>
     * dom validation 은 하지 않는다. 전체 완성된 dom 에 대해서 validation 한다.
     * 
     * @param includeBlock
     *            include Block Node
     * @param dynamicConfig
     *            include 된 Block
     * @return Node
     * @throws Exception
     *             exception
     * */
    private Node importIncludeBlock(Node includeBlock, String dynamicConfig)
            throws Exception {
        File file = new File(UsfDispatcher.getInstance().getRootPath()
                + dynamicConfig);
        InputStream is = null;

        try {
            is = new FileInputStream(file);
            log.debug("Get include bloc from file : " + file.getAbsolutePath());
        } catch (FileNotFoundException e) {
            // Added by imjavaj
            // Check include block in dependency jars.
            ResourcePatternResolver resourceLoader = new PathMatchingResourcePatternResolver();
            Resource[] resources = resourceLoader.getResources("classpath*:"
                    + dynamicConfig.substring(1));

            if (resources != null && resources.length > 0) {
                is = resources[0].getInputStream();
                log.debug("Get include bloc from dependency jar : "
                        + resources[0].getURL());
            }
        }

        Document document = null;

        try {
            document = XMLDOMUtil.loading(is, true, entityResolver);
        } finally {
            is.close();
        }

        NodeList nodes = document.getDocumentElement().getChildNodes();

        for (int i = 0; i < nodes.getLength(); i++) {
            Node node = nodes.item(i);
            Node replaceIncludeBlock = includeBlock.getOwnerDocument()
                    .importNode(node, true);
            includeBlock.getParentNode().insertBefore(replaceIncludeBlock,
                    includeBlock);
        }

        includeBlock.getParentNode().removeChild(includeBlock);
        return null;
    }

    // ---------------------------------------------------------------------------------------------------------------------------

    /**
     * include page 정보 추출.
     * 
     * @param pcfgRunElement
     *            the Node
     * @param pageInfo
     *            the PageInfo
     * @throws Exception
     *             the Exception
     */
    private void extractInclude(Node pcfgRunElement, PageInfo pageInfo)
            throws Exception {
        String include = XMLDOMUtil.getValue(pcfgRunElement, " :include");
        pageInfo.setInclude(StringUtil.trim(include));

    }

    /**
     * {@link Action} 수행을 위한 Global Permission 추출.
     * 
     * @param pcfgRunElement
     *            the Node
     * @param pageInfo
     *            the PageInfo
     * @throws Exception
     *             the Exception
     */
    private void extractGlobalPermission(Node pcfgRunElement, PageInfo pageInfo)
            throws Exception {
        String globalPermission = XMLDOMUtil.getValue(pcfgRunElement,
                " :page-permission");
        pageInfo.setGlobalPermission(globalPermission);
    }

    /**
     * {@link Action} 수행을 위한 Global {@link ResultPageInfos} 추출.
     * 
     * @param pcfgRunElement
     *            the Node
     * @param pageInfo
     *            the PageInfo
     * @throws Exception
     *             the Exception
     */
    private void extractGlobalResult(Node pcfgRunElement, PageInfo pageInfo)
            throws Exception {
        Node globalResultNode = XMLDOMUtil.getNode(pcfgRunElement,
                " :page-result");
        Node commonScriptNodes = XMLDOMUtil.getNode(globalResultNode,
                " :condition-script");

        if (commonScriptNodes != null) {
            String scriptType = XMLDOMUtil.getValue(commonScriptNodes, "@type");
            String funcName = XMLDOMUtil.getValue(commonScriptNodes,
                    "@func-name");
            String params = XMLDOMUtil.getValue(commonScriptNodes, "@params");
            String returnId = XMLDOMUtil.getValue(commonScriptNodes,
                    "@return-id");

            ScriptInfo scriptInfo = new ScriptInfo(scriptType, funcName);
            scriptInfo.setParams(params);
            scriptInfo.setReturnId(returnId);
            pageInfo.setPageCommonScriptInfo(scriptInfo);
        }

        if (globalResultNode != null) {
            pageInfo.setGlobalSuccessResultInfo(getResultPageInfo(
                    globalResultNode, " :success"));
            pageInfo.setGlobalFailureResultInfo(getResultPageInfo(
                    globalResultNode, " :failure"));
        }
    }

    /**
     * {@link Action} {@link List} 추출.
     * 
     * @param pcfgRunElement
     *            the Node
     * @param pageInfo
     *            the PageInfo
     * @throws Exception
     *             the Exception
     */
    private void extractAction(Node pcfgRunElement, PageInfo pageInfo)
            throws Exception {
        Iterator<Node> actions = XMLDOMUtil.getNodeIterator(pcfgRunElement,
                " :action");

        while (actions.hasNext()) {
            Node actionNode = actions.next();
            String actionType = XMLDOMUtil.getValue(actionNode, "@type");
            actionIgnoreException = XMLDOMUtil.getValue(actionNode,
                    "@ignore-exception");
            Action actionInfo = new Action(XMLDOMUtil.getValue(actionNode,
                    "@id"), actionType);

            if (actionType != null && actionType.equals("common")) {
                pageInfo.setCommonAction(actionInfo);
            }

            actionInfo.setInterceptor(XMLDOMUtil.getValue(actionNode,
                    "@interceptor"));

            extractPermission(actionNode, actionInfo);
            extractResult(actionNode, actionInfo);
            extractServices(actionNode, actionInfo);
            extractInterceptors(actionNode, actionInfo);
            pageInfo.addActionInfo(actionInfo.getId(), actionInfo);
        }
    }

    /**
     * {@link Action} 내의 Interceptors 추출.
     * 
     * @param actionNode
     *            the Node
     * @param actionInfo
     *            the Action
     */
    private void extractInterceptors(Node actionNode, Action actionInfo) {
        Node interceptorsNode = XMLDOMUtil
                .getNode(actionNode, " :interceptors");

        if (interceptorsNode == null) {
            return;
        }

        interceptorsNode.getAttributes().getNamedItem("exclude");
        Iterator<Node> children = XMLDOMUtil.getChildIterator(interceptorsNode);

        while (children.hasNext()) {
            Node interceptorNode = children.next();

            if (interceptorNode.getNodeType() == Node.ELEMENT_NODE
                    || interceptorNode.getNodeName().equals("interceptor-ref")) {

                String name = interceptorNode.getAttributes()
                        .getNamedItem("name").getNodeValue();
                RuntimeInterceptorConfig interceptorInfo = new RuntimeInterceptorConfig(
                        name);
                interceptorInfo.setParams(getParams(interceptorNode));
                actionInfo.addRuntimeInterceptor(interceptorInfo);
            }

        }
    }

    /**
     * Interceptor에 전달된 Parameter 정보.
     * 
     * @param interceptorNode
     *            the Node
     * @return the parameter information
     */
    private Map<String, String> getParams(Node interceptorNode) {
        NodeList paramNodes = interceptorNode.getChildNodes();
        Map<String, String> params = new HashMap<String, String>();

        for (int i = 0; i < paramNodes.getLength(); i++) {
            Node paramNode = paramNodes.item(i);

            if ("parameter".equals(paramNode.getNodeName())) {
                String name = paramNode.getAttributes().getNamedItem("name")
                        .getNodeValue();
                String value = XMLDOMUtil.getElementText(paramNode);
                params.put(name, value);
            }

        }

        return params;
    }

    /**
     * {@link Action} 내의 {@link PermissionInfo} 추출.
     * 
     * @param actionNode
     *            the Node
     * @param actionInfo
     *            the Action
     * @throws Exception
     *             the Exception
     */
    private void extractPermission(Node actionNode, Action actionInfo)
            throws Exception {
        Node permissionNode = XMLDOMUtil.getNode(actionNode, " :permission");

        if (permissionNode == null) {
            return;
        }

        PermissionInfo permissionInfo = new PermissionInfo(XMLDOMUtil.getValue(
                permissionNode, "@type"),
                XMLDOMUtil.getElementText(permissionNode));
        actionInfo.setPermissionInfo(permissionInfo);
    }

    /**
     * {@link Action} 내의 Success, Failure 각각에 대한 {@link ResultPageInfos} 추출.
     * 
     * @param actionNode
     *            the Node
     * @param actionInfo
     *            the Action
     * @throws Exception
     *             the Exception
     */
    private void extractResult(Node actionNode, Action actionInfo)
            throws Exception {
        Node resultNode = XMLDOMUtil.getNode(actionNode, " :result");
        Node commonScriptNodes = XMLDOMUtil.getNode(resultNode,
                " :condition-script");

        if (commonScriptNodes != null) {
            ScriptInfo scriptInfo = extractConditionScript(commonScriptNodes);
            actionInfo.setResultCommonScriptInfo(scriptInfo);
        }

        actionInfo.setSuccessResultInfo(getResultPageInfo(resultNode,
                " :success"));
        actionInfo.setFailureResultInfo(getResultPageInfo(resultNode,
                " :failure"));
    }

    /**
     * {@link BusinessObject} {@link List} 추출.
     * 
     * @param actionNode
     *            the Node
     * @param actionInfo
     *            the Action
     * @throws Exception
     *             the Exception
     */
    private void extractServices(Node actionNode, Action actionInfo)
            throws Exception {
        Node serviceNode = XMLDOMUtil.getNode(actionNode, " :services");

        if (serviceNode == null) {
            return;
        }

        Iterator<Node> children = XMLDOMUtil.getChildIterator(serviceNode);

        while (children.hasNext()) {
            Node child = children.next();

            if (child.getNodeType() != Node.ELEMENT_NODE) {
                continue;
            }

            String nodeName = child.getNodeName();

            if ("control".equals(nodeName)) {
                List<Service> services = extractServices(null, child);

                // 여기서 common Script 처리

                actionInfo.addControlBlockInfoAll(services);
            } else {
                Service service = extractService(null, child);
                actionInfo.addBusinessObjectInfo(service);
            }
        }
    }

    /**
     * Extract bo group.
     * 
     * @param boGroupNode
     *            the bo group node
     * @return the business object group
     * @throws Exception
     *             the exception
     */
    private BusinessObjectGroup extractBOGroup(Node boGroupNode)
            throws Exception {
        String scriptType = XMLDOMUtil.getValue(boGroupNode, "@type");
        String funcName = XMLDOMUtil.getValue(boGroupNode, "@func-name");
        ScriptInfo scriptInfo = new ScriptInfo(scriptType, funcName);
        BusinessObjectGroup boGroup = new BusinessObjectGroup(scriptInfo);
        Iterator<Node> children = XMLDOMUtil.getChildIterator(boGroupNode);

        while (children.hasNext()) {
            Node boNode = children.next();

            if (boNode.getNodeType() != Node.ELEMENT_NODE) {
                continue;
            }

            // 하위 호환성을 위해 두곳 모두 입력한다.
            BusinessObject bo = extractBO(boNode);
            bo.setParent(boGroup);
            boGroup.addBoInfos(bo);
            boGroup.addServiceInfo(bo);
        }

        return boGroup;
    }

    /**
     * {@code node} 를 파싱해서 {@link Service} 를 반환한다. <br/> {@code node} 는
     * condition-script, bo, bo-group, method, if, elseif, if-group, else 중 하나
     * 
     * @param parent
     *            상위 {@link Service}
     * @param node
     *            Node
     * @return Service
     * @throws Exception
     *             exception
     * */
    private Service extractService(Service parent, Node node) throws Exception {
        String nodeName = node.getNodeName();
        Service service = null;

        if ("condition-script".equals(nodeName)) {
            service = extractConditionScript(node);
        } else if ("bo".equals(nodeName)) {
            service = extractBO(node);
        } else if ("bo-group".equals(nodeName)) {
            service = extractBOGroup(node);
        } else if ("method".equals(nodeName)) {
            service = getMethodInfo(node);
        } else if ("if".equals(nodeName) || "elseif".equals(nodeName)
                || "when".equals(nodeName)) {
            service = extractIf(node);
        } else if ("if-group".equals(nodeName) || "choose".equals(nodeName)) {
            service = extractIfGroup(node);
        } else if ("else".equals(nodeName) || "otherwise".equals(nodeName)) {
            service = extractIf(node, "true");
        } else if ("assign".equals(nodeName)) {
            service = extractAssign(node);
        } else {
            throw new IllegalStateException("wrong element : " + nodeName);
        }

        ((ServiceImpl) service).setNodeName(nodeName);
        ((ServiceImpl) service).setParent(parent);
        return service;
    }

    /**
     * @param node
     *            &lt;assign&gt; Element
     * @return {@link Assign}
     * */
    private Service extractAssign(Node node) {
        String script = XMLDOMUtil.getElementText(node);
        Assign assign = new Assign(script);
        return assign;
    }

    /**
     * {@code node} 의 child node {@link Service} 리스트를 추출한다.
     * 
     * @param node
     *            Node
     * @param parent
     *            상위 {@link Service}
     * @return {@code List<Service>}
     * @throws Exception
     *             exception
     * */
    private List<Service> extractServices(Service parent, Node node)
            throws Exception {
        NodeList nodes = node.getChildNodes();
        List<Service> services = new ArrayList<Service>();

        for (int i = 0; i < nodes.getLength(); i++) {
            Node childNode = nodes.item(i);

            if (childNode.getNodeType() != Node.ELEMENT_NODE) {
                continue;
            }

            Service service = extractService(parent, childNode);
            services.add(service);
        }

        return services;
    }

    /**
     * {@code node} 에서 if-group 을 추출한다.
     * 
     * @param node
     *            Node
     * @return ServiceConditionGroup
     * @throws Exception
     *             exception
     * */
    private ServiceConditionGroup extractIfGroup(Node node) throws Exception {
        ServiceConditionGroup serviceConditionGroup = new ServiceConditionGroup();
        List<Service> services = extractServices(serviceConditionGroup, node);
        serviceConditionGroup.addServiceInfoAll(services);

        return serviceConditionGroup;
    }

    /**
     * {@code node} 에서 if 를 추출한다.
     * 
     * @param node
     *            Node
     * @return {@link ServiceCondition}
     * @throws Exception
     *             exception
     * */
    private ServiceCondition extractIf(Node node) throws Exception {
        return extractIf(node, XMLDOMUtil.getValue(node, " :condition"));
    }

    /**
     * {@code node} 에서 if 를 추출한다.
     * 
     * @param node
     *            추출할 node
     * @param condition
     *            if 구문의 검사 조건 (else 일 경우 default true 로 넣기위해 추가함)
     * @return {@link ServiceCondition}
     * @throws Exception
     *             exception
     * */
    private ServiceCondition extractIf(Node node, String condition)
            throws Exception {
        ServiceCondition serviceCondition = new ServiceCondition();

        if (StringUtil.isEmpty(condition)) {
            log.warn("condition sentence is empty : " + getActionId(node)
                    + " Action");
        }

        serviceCondition.setCondition(condition);
        serviceCondition.setCheck(true);

        Iterator<Node> boNodes = XMLDOMUtil.getChildIterator(node);

        while (boNodes.hasNext()) {
            Node child = boNodes.next();

            if (child.getNodeType() != Node.ELEMENT_NODE) {
                continue;
            }

            if ("condition".equals(child.getNodeName())) {
                continue;
            }

            Service service = extractService(serviceCondition, child);

            if (ServiceType.condition_script == service.getServiceType()) {
                serviceCondition.setScriptInfo((ScriptInfo) service);
            } else if (ServiceType.method == service.getServiceType()) {
                // if 밑에 bo 없이 method 가 나오는 경우 bo 만들어서 넣어준다. (method 마다 bo 가 하나씩
                // 생성됨).
                BusinessObject bo = new BusinessObject(getBoId(node));
                bo.setNodeName(ServiceType.bo.name());
                bo.addServiceInfo(service);
                bo.setParent(serviceCondition);
                serviceCondition.addServiceInfo(bo);
            } else {
                serviceCondition.addServiceInfo(service);
            }
        }

        return serviceCondition;
    }

    /**
     * node 의 Parent 에서 bo 를 찾아서 id 를 반환한다.
     * 
     * @param node
     *            Node
     * @return bo id
     * @throws Exception
     *             exception
     * */
    private String getBoId(Node node) throws Exception {
        String boId = null;

        for (Node parent = node; parent.getParentNode() != null; parent = parent
                .getParentNode()) {
            if ("bo".equals(parent.getNodeName())) {
                boId = XMLDOMUtil.getValue(parent, "@id");
                boId = StringUtil.replaceModuleName(boId);
            }
        }

        if (boId == null) {
            throw new IllegalStateException("method has no bo id ");
        }

        return boId;
    }

    /**
     * node 를 포함한 Action 의 id 를 가져온다.
     * 
     * @param node
     *            Node
     * @return Action id
     * */
    private String getActionId(Node node) {
        while ((node = node.getParentNode()) != null) {
            if ("action".equals(node.getNodeName().toLowerCase())) {
                return XMLDOMUtil.getValue(node, "@id");
            }
        }

        return null;
    }

    /**
     * {@code node} 를 파싱해서 {@link BO} 를 반환한다.
     * 
     * @param node
     *            Node
     * @return {@link BusinessObject}
     * @throws Exception
     *             exception
     * */
    private BusinessObject extractBO(Node node) throws Exception {
        String boId = XMLDOMUtil.getValue(node, "@id");
        String replacevalue = StringUtil.replaceModuleName(boId);
        BusinessObject boInfo = new BusinessObject(replacevalue, true);
        String connectorId = XMLDOMUtil.getValue(node, "@connector");

        if (StringUtil.isNotEmpty(connectorId, false)) {
            boInfo.setConnectorId(connectorId);
        }

        Iterator<Node> subNodes = XMLDOMUtil.getChildIterator(node);

        // 하위 노드 탐색
        while (subNodes.hasNext()) {
            Node subNode = subNodes.next();

            if (subNode.getNodeType() != Node.ELEMENT_NODE) {
                continue;
            }

            Service subService = extractService(boInfo, subNode);
            boInfo.addServiceInfo(subService);
        }

        return boInfo;
    }

    /**
     * {@code node} 를 파싱해서 {@link ConditionScript} 를 반환한다.
     * 
     * @param node
     *            Node
     * @return {@link ScriptInfo}
     * */
    private ScriptInfo extractConditionScript(Node node) {
        String scriptType = XMLDOMUtil.getValue(node, "@type");
        String funcName = XMLDOMUtil.getValue(node, "@func-name");
        String params = XMLDOMUtil.getValue(node, "@params");
        String returnId = XMLDOMUtil.getValue(node, "@return-id");

        ScriptInfo scriptInfo = new ScriptInfo(scriptType, funcName);
        scriptInfo.setParams(params);
        scriptInfo.setReturnId(returnId);
        return scriptInfo;
    }

    /**
     * Gets the method info.
     * 
     * @param methodNode
     *            the method node
     * @return the method info
     */
    private Method getMethodInfo(Node methodNode) {
        Method methodInfo = new Method(XMLDOMUtil.getValue(methodNode, "@id"),
                XMLDOMUtil.getValue(methodNode, "@return-id"));
        String isParamMap = XMLDOMUtil.getValue(methodNode, "@use-parammap");
        methodInfo.setIsParameterMap(Boolean.valueOf(isParamMap));

        String useCache = XMLDOMUtil.getValue(methodNode, "@cache");
        String expire = XMLDOMUtil.getValue(methodNode, "@expire");
        methodInfo.setUseCache(Boolean.valueOf(useCache));
        String methodIgnoreException = XMLDOMUtil.getValue(methodNode,
                "@ignore-exception");

        if (!StringUtil.isEmpty(methodIgnoreException)) {
            methodInfo.setIsIgnoreException(Boolean
                    .valueOf(methodIgnoreException));
        } else if (!StringUtil.isEmpty(actionIgnoreException)) {
            methodInfo.setIsIgnoreException(Boolean
                    .valueOf(actionIgnoreException));
        } else {
            methodInfo.setIsIgnoreException(Boolean
                    .valueOf(globalIgnoreException));
        }

        if (StringUtil.isNotEmpty(expire)) {
            methodInfo.setExpire(Integer.valueOf(expire));
        } else {
            methodInfo.setExpire(-1);
        }

        Iterator<Node> params = XMLDOMUtil.getNodeIterator(methodNode,
                " :param");

        while (params.hasNext()) {
            Node paramNode = params.next();

            String id = XMLDOMUtil.getValue(paramNode, "@id");
            String ref = XMLDOMUtil.getValue(paramNode, "@ref");
            String defaultValue = XMLDOMUtil.getValue(paramNode, "@default");

            ParameterInfo parameterInfo = new ParameterInfo(id, ref,
                    defaultValue);
            methodInfo.addParameterInfo(parameterInfo);
        }

        Iterator<Node> script = XMLDOMUtil.getNodeIterator(methodNode,
                " :script");

        while (script.hasNext()) {
            Node scriptNode = script.next();

            String scriptType = XMLDOMUtil.getValue(scriptNode, "@type");
            String funcName = XMLDOMUtil.getValue(scriptNode, "@func-name");
            String callType = XMLDOMUtil.getValue(scriptNode, "@call-type");

            ScriptInfo scriptInfo = new ScriptInfo(scriptType, funcName);
            scriptInfo.setCallType(callType);
            methodInfo.addScriptInfo(scriptInfo);
        }

        return methodInfo;
    }

    // ---------------------------------------------------------------------------------------------------------------------------

    /**
     * {@link ResultPageInfos} 추출. {@link ConditionInfo} 와 default
     * {@link ResultPageInfo} 둘다 없는 경우 널을 반환한다.
     * 
     * @param parentNode
     *            {@code global-result} 또는 {@code result} 에 대한 {@link Node}.
     * @param type
     *            {@code success} 또는 {@code failure}
     * @return the ResultPageInfos
     * @throws Exception
     *             the Exception
     */
    private ResultPageInfos getResultPageInfo(Node parentNode, String type)
            throws Exception {
        ResultPageInfos resultPageInfo = new ResultPageInfos();

        // success/ failure
        Node statusNode = XMLDOMUtil.getNode(parentNode, type);

        if (statusNode == null) {
            return null;
        }

        // conditon.
        List<ResultCondition> conditions = getResultCondition(statusNode);

        for (ResultCondition condition : conditions) {
            resultPageInfo.addCondition(condition);
        }

        // default-page
        resultPageInfo.setDefaultResultPageInfo(getPageInfo(XMLDOMUtil.getNode(
                statusNode, " :page")));

        // condition과 defaultPage 가 없는 경우는 널을 반환.
        if (resultPageInfo.getConditions().size() == 0
                && resultPageInfo.getDefaultResultPageInfo() == null) {
            return null;
        }

        return resultPageInfo;
    }

    /**
     * {@code statusNode} 에서 Condition 조건 (if, choose) 를 추출해서 반환한다.
     * 
     * @param statusNode
     *            result 하위의 success, failure 조건
     * @return 추출한 {@link ResultCondition} 목록
     * @throws Exception
     *             exception
     * */
    private List<ResultCondition> getResultCondition(Node statusNode)
            throws Exception {
        List<ResultCondition> conditions = new ArrayList<ResultCondition>();
        NodeList nodeList = statusNode.getChildNodes();

        for (int i = 0; i < nodeList.getLength(); i++) {
            Node node = nodeList.item(i);

            if (node.getNodeType() != Node.ELEMENT_NODE) {
                continue;
            }

            String nodeName = node.getNodeName();
            ResultCondition condition = null;

            if ("if".equals(nodeName) || "when".equals(nodeName)) {
                condition = getResultConditionIf(node,
                        XMLDOMUtil.getValue(node, " :condition"));
            } else if ("choose".equals(nodeName)) {
                condition = getResultConditionGroup(node);
            } else if ("otherwise".equals(nodeName)) {
                condition = getResultConditionIf(node, "true");
            } else {
                continue;
            }

            conditions.add(condition);
        }

        return conditions;
    }

    /**
     * choose 를 파싱한다.
     * 
     * @param node
     *            파싱할 choose node
     * @return 파싱한 {@link ResultCondition}
     * @throws Exception
     *             exception
     * */
    private ResultCondition getResultConditionGroup(Node node) throws Exception {
        ConditionInfoGroup group = new ConditionInfoGroup();
        List<ResultCondition> conditions = getResultCondition(node);

        for (ResultCondition condition : conditions) {
            group.addConditionInfo((ConditionInfo) condition);
        }

        return group;
    }

    /**
     * if/when 을 파싱한다.
     * 
     * @param node
     *            Condition 조건을 갖고있는 Node
     * @param conditionString
     *            Condition 조건절
     * @return 추출한 {@link ResultCondition}
     * @throws Exception
     *             the Exception
     * */
    private ResultCondition getResultConditionIf(Node node,
            String conditionString) throws Exception {
        Node page = XMLDOMUtil.getNode(node, " :page");
        ConditionInfo conditionInfo = null;

        if (page != null) {
            ResultPageInfo pageInfo = getPageInfo(page);
            conditionInfo = new ConditionInfo(conditionString, pageInfo);
            Node scriptNodes = XMLDOMUtil.getNode(node, " :condition-script");

            if (scriptNodes != null) {
                ScriptInfo scriptInfo = extractConditionScript(scriptNodes);
                conditionInfo.setScriptInfo(scriptInfo);
            }
        } else {
            conditionInfo = new ConditionInfo(conditionString, null);
            List<ResultCondition> conditions = getResultCondition(node);

            for (ResultCondition condition : conditions) {
                conditionInfo.addSubCondition(condition);
            }
        }

        return conditionInfo;
    }

    /**
     * {@link ResultPageInfo} 생성. Page 노드가 없는 경우 null 을 반환한다.
     * 
     * @param pageNode
     *            Page 정보를 갖고있는 Node
     * @return the ResultPageInfo
     * @throws Exception
     *             the Exception
     */
    private ResultPageInfo getPageInfo(Node pageNode) throws Exception {
        if (pageNode == null) {
            return null;
        }

        String type = XMLDOMUtil.getValue(pageNode, "@type");
        String src = XMLDOMUtil.getElementText(pageNode);

        Properties properties = null;
        Properties requestParams = null;

        if (StringUtil.isEmpty(src, true)) {
            properties = new Properties();
            Iterator<Node> propertyIterator = XMLDOMUtil.getNodeIterator(
                    pageNode, " :property");

            while (propertyIterator.hasNext()) {
                Node propertyNode = propertyIterator.next();
                properties.setProperty(
                        XMLDOMUtil.getValue(propertyNode, "@name"),
                        XMLDOMUtil.getElementText(propertyNode));
            }

            requestParams = new Properties();
            Iterator<Node> requestParamsIterator = XMLDOMUtil.getNodeIterator(
                    pageNode, " :request-params/ :request-param");

            while (requestParamsIterator.hasNext()) {
                Node requestParamNode = requestParamsIterator.next();
                requestParams.setProperty(
                        XMLDOMUtil.getValue(requestParamNode, "@name"),
                        XMLDOMUtil.getElementText(requestParamNode));
            }

            if (properties.containsKey("src")) {
                src = properties.getProperty("src");
            }
        }

        if (src == null) {
            return null;
        }

        src = src.trim();
        String replacevalue = StringUtil.replacePageSrc(src);

        ResultPageInfo pageInfo = new ResultPageInfo(
                DISPATCHER_TYPE.getDispatcherType(type), replacevalue);

        pageInfo.setId(String.valueOf(System.currentTimeMillis()));
        pageInfo.setProperties(properties);
        pageInfo.setRequestParams(requestParams);

        return pageInfo;
    }
}
